/**
 * @file
 * @brief This file contains the stream based message handler.
 */

#ifndef NOSAL_LOG_LOG_STREAM_H_
#define NOSAL_LOG_LOG_STREAM_H_

#include "nosal/log/common.h"

#include <chrono>
#include <iomanip>
#include <sstream>
#include <type_traits>
#include <utility>

namespace netaos {
namespace nosal {
namespace log {

enum class Fmt : std::uint16_t
{
    kDefault = 0,  ///< implementation-defined formatting
    kDec = 1,  ///< decimal (signed/unsigned)
    kOct = 2,  ///< octal
    kHex = 3,  ///< hexadecimal
    kBin = 4,  ///< binary
    kDecFloat = 5,  ///< decimal float (like printf "%f")
    kEngFloat = 6,  ///< engineering float (like printf "%e")
    kHexFloat = 7,  ///< hex float (like printf "%a")
    kAutoFloat = 8,  ///< automatic "shortest" float (like printf "%g")
};

struct Format final
{
    Fmt fmt;

    std::uint16_t precision;
};

constexpr Format Dflt() noexcept
{
    return Format{Fmt::kDefault, 0};
}

constexpr Format Hex() noexcept
{
    return Format{Fmt::kHex, 65535};
}

constexpr Format Hex(std::uint16_t precision) noexcept
{
    return Format{Fmt::kHex, precision};
}

constexpr Format Dec() noexcept
{
    return Format{Fmt::kDec, 65535};
}

constexpr Format Dec(std::uint16_t precision) noexcept
{
    return Format{Fmt::kDec, precision};
}

constexpr Format Oct() noexcept
{
    return Format{Fmt::kOct, 65535};
}

constexpr Format Oct(std::uint16_t precision) noexcept
{
    return Format{Fmt::kOct, precision};
}

constexpr Format Bin() noexcept
{
    return Format{Fmt::kBin, 65535};
}

constexpr Format Bin(std::uint16_t precision) noexcept
{
    return Format{Fmt::kBin, precision};
}

constexpr Format DecFloat(std::uint16_t precision = 6) noexcept
{
    return Format{Fmt::kDecFloat, precision};
}

constexpr Format DecFloatMax() noexcept
{
    return Format{Fmt::kDecFloat, 65535};
}

constexpr Format EngFloat(std::uint16_t precision = 6) noexcept
{
    return Format{Fmt::kEngFloat, precision};
}

constexpr Format EngFloatMax() noexcept
{
    return Format{Fmt::kEngFloat, 65535};
}

constexpr Format HexFloat(std::uint16_t precision) noexcept
{
    return Format{Fmt::kHexFloat, precision};
}

constexpr Format HexFloatMax() noexcept
{
    return Format{Fmt::kHexFloat, 65535};
}

constexpr Format AutoFloat(std::uint16_t precision = 6) noexcept
{
    return Format{Fmt::kAutoFloat, precision};
}

constexpr Format AutoFloatMax() noexcept
{
    return Format{Fmt::kAutoFloat, 65535};
}

/**
 * @defgroup logstream Log Stream
 * @brief Stream based message handler.
 */

/**
 * @ingroup logstream
 * @class LogStream
 *
 * @brief Class representing a log message.
 *
 * Insert stream operators are to be used for appending data.
 *
 * @note Normally, you would not use this class directly, but use one of the log methods provided in
 * the main logging API or an instance of the Logger class instead. Those methods automatically setup
 * an object of this class with the given log severity level. The only reason to get in touch with this
 * class directly is if you want to hold a @c LogStream object longer than the default one-statement scope.
 * This is useful in order to create log messages that are distributed over multiple code lines.
 * See @c Flush() method for further information.
 *
 * Every call on the insert stream operator against this object will be processed as a call to the
 * underlying DLT back-end, which means that the message buffer is going to be filled with payload data.
 *
 * Once this object gets out of scope (in normal usage as soon as the statement ends), its @c dtor is
 * calling the message flush command on the DLT back-end which marks the message buffer as ready to be
 * sent out remotely.
 *
 * @c LogStream supports all of the primitive data types (PoDs) natively. However, it can be easily extended
 * for your own complex types by providing a stream operator that makes use of already supported types.
 *
 * @code
 * struct MyCustomType {
 *   int8_t foo;
 *   std::string bar;
 * };
 *
 * LogStream& operator<<(LogStream& out, const MyCustomType& value) {
 *     return (out << value.foo << value.bar);
 * }
 *
 * g_loggerCtx0.LogDebug() << MyCustomType{42, "The answer is"};
 * @endcode
 */

class Logger;  // forward declare Logger class

/**
 * Helper struct that is utilized as custom type.
 * Holds an integer value that will be logged with a special format.
 */
struct LogHex8
{
    uint8_t value;
};

struct LogHex16
{
    uint16_t value;
};

struct LogHex32
{
    uint32_t value;
};

struct LogHex64
{
    uint64_t value;
};

struct LogBin8
{
    uint8_t value;
};

struct LogBin16
{
    uint16_t value;
};

struct LogBin32
{
    uint32_t value;
};

struct LogBin64
{
    uint64_t value;
};

template <typename T>
struct Argument
{
    const T& value;
    const std::string& name;
    const std::string& unit;
    Format fmt;
};

class LogStream final
{
public:
    LogStream() = delete;

    /**
     * @brief Creates the message stream object.
     *
     * Initiates it with the given log level directly on the back-end.
     *
     * @param[in] loglevel  The severity level of this message
     * @param[in] logger    The associated logger context
     */
    LogStream(LogLevel loglevel, const Logger& logger) noexcept;

    /**
     * @brief Flushes the log message before destroying the stream object.
     *
     * Unless it was not explicitly flushed before, by calling @c Flush().
     */
    ~LogStream();

    /*
     * The big five.
     *
     * We actually don't want this class movable or copyable, but enforce RVO instead.
     * Since "Guaranteed copy elision" will be supported up from C++17, declaring the move ctor
     * without definition is a workaround and is necessary because we have a custom dtor.
     * With the missing move ctor implementation, explicit calls of std::move() on objects
     * of this class, will be prevented by a compile error.
     */
    LogStream(const LogStream&) = delete;
    LogStream& operator=(const LogStream&) = delete;
    LogStream(LogStream&&);
    LogStream& operator=(LogStream&&) = delete;

    /**
     * @brief Processes the current log buffer.
     *
     * And prepares the object for new message in one go. Further usage of the @c LogStream object
     * after calling this method will end up in a new message. Use this method in case you want to
     * build your message distributed over multiple code lines.
     *
     * @code
     * LogStream logInfoBuilder{g_loggerCtx0.LogInfo()};
     * logInfoBuilder << "message builder...";
     *
     * logInfoBuilder << 123;
     * logInfoBuilder << "one more param.";
     * logInfoBuilder << 4.56;
     * logInfoBuilder << "we're done...";
     * logInfoBuilder.Flush(); // sends out what is in the buffer as one single message
     *
     * logInfoBuilder << "new message initiated...";
     * @endcode
     *
     * @note Calling @c Flush() is only necessary if the @c LogStream object is going to be re-used within
     * the same scope. Otherwise, if the object goes out of scope (e.g. end of function block), than
     * flush operation will be anyway done internally by the @c dtor.
     */
    void Flush() noexcept;

    /// Build-in support for native types.
    /// @{
    /**
     * @brief Writes boolean parameter into message.
     */
    LogStream& operator<<(bool value) noexcept;
    /**
     * @brief Writes unsigned int 8 bit parameter into message.
     */
    LogStream& operator<<(uint8_t value) noexcept;
    /**
     * @brief Writes unsigned int 16 bit parameter into message.
     */
    LogStream& operator<<(uint16_t value) noexcept;
    /**
     * @brief Writes unsigned int 32 bit parameter into message.
     */
    LogStream& operator<<(uint32_t value) noexcept;
    /**
     * @brief Writes unsigned int 64 bit parameter into message.
     */
    LogStream& operator<<(uint64_t value) noexcept;
    /**
     * @brief Writes signed int 8 bit parameter into message.
     */
    LogStream& operator<<(int8_t value) noexcept;
    /**
     * @brief Writes signed int 16 bit parameter into message.
     */
    LogStream& operator<<(int16_t value) noexcept;
    /**
     * @brief Writes signed int 32 bit parameter into message.
     */
    LogStream& operator<<(int32_t value) noexcept;
    /**
     * @brief Writes signed int 64 bit parameter into message.
     */
    LogStream& operator<<(int64_t value) noexcept;
    /**
     * @brief Writes float 32 bit parameter into message.
     */
    LogStream& operator<<(float value) noexcept;
    /**
     * @brief Writes float 64 bit parameter into message.
     */
    LogStream& operator<<(double value) noexcept;
    /// @}

    /// @name special types
    /// Build-in support for DLT special format types.
    /// @{

    /**
     * @brief Writes unsigned int parameter into message, formatted as hexadecimal 8 digits.
     */
    LogStream& operator<<(const LogHex8& value) noexcept;

    /**
     * @brief Writes unsigned int parameter into message, formatted as hexadecimal 16 digits.
     */
    LogStream& operator<<(const LogHex16& value) noexcept;

    /**
     * @brief Writes unsigned int parameter into message, formatted as hexadecimal 32 digits.
     */
    LogStream& operator<<(const LogHex32& value) noexcept;

    /**
     * @brief Writes unsigned int parameter into message, formatted as hexadecimal 64 digits.
     */
    LogStream& operator<<(const LogHex64& value) noexcept;

    /**
     * @brief Writes unsigned int parameter into message, formatted as binary 8 digits.
     */
    LogStream& operator<<(const LogBin8& value) noexcept;

    /**
     *  @brief Writes unsigned int parameter into message, formatted as binary 16 digits.
     */
    LogStream& operator<<(const LogBin16& value) noexcept;

    /**
     *  @brief Writes unsigned int parameter into message, formatted as binary 32 digits.
     */
    LogStream& operator<<(const LogBin32& value) noexcept;

    /**
     *  @brief Writes unsigned int parameter into message, formatted as binary 64 digits.
     */
    LogStream& operator<<(const LogBin64& value) noexcept;

    /**
     *  @brief Writes nosal::core::StringView into message.
     */
    LogStream& operator<<(const std::string value) noexcept;

    /**
     *  @brief Writes null terminated UTF8 string into message. (NOT sPECIFIED. WILL BE REMOVED IN FUTURE!)
     */
    LogStream& operator<<(const char* value) noexcept;

    LogStream& operator<<(const std::vector<uint8_t> value) noexcept;

    LogStream& operator<<(LogLevel value) noexcept;
    /**
     * @brief Add source file location into the message
     *
     * This function has no effect if another member function that adds
     * content to the current message has already been called.
     *
     * @param file the source file identifier
     * @param line the source file line number
     * @returns *this
     */
    LogStream& WithLocation(std::string file, int line) noexcept;


    //friend LogStream& operator<<(LogStream& out, const nosal::core::ErrorCode& value) noexcept;

    /**
     * @brief Write a byte sequence into message
     *
     * This call shall copy the sequence of core::Byte objects as-is into the message.
     *
     * When output to the console, this byte sequence shall be shown
     * as a sequence of apostrophe-separated list of hexadecimal octet-pairs,
     * for instance: "48'65'6c'6c'6f"
     *
     * @param data a Span<const Byte> covering the range to be logged
     * @returns *this
     */
    //gcy LogStream& operator<<(nosal::core::Span<const nosal::core::Byte> data) noexcept;
    /// @}

    /**
     * @brief Writes an argument with attributes into the message
     */
    template <typename T>
    LogStream& operator<<(const Argument<T>& arg) noexcept
    {
        if (isInitialized()) {
            putValue(arg.value, arg.name, arg.unit, arg.fmt);
        }
        return *this;
    }

private:
    bool isInitialized() const noexcept;

    void putValue(uint8_t value, const std::string& name, const std::string& unit, Format const& fmt) noexcept;
    void putValue(uint16_t value, const std::string& name, const std::string& unit, Format const& fmt) noexcept;
    void putValue(uint32_t value, const std::string& name, const std::string& unit, Format const& fmt) noexcept;
    void putValue(uint64_t value, const std::string& name, const std::string& unit, Format const& fmt) noexcept;
    void putValue(int8_t value, const std::string& name, const std::string& unit, Format const& fmt) noexcept;
    void putValue(int16_t value, const std::string& name, const std::string& unit, Format const& fmt) noexcept;
    void putValue(int32_t value, const std::string& name, const std::string& unit, Format const& fmt) noexcept;
    void putValue(int64_t value, const std::string& name, const std::string& unit, Format const& fmt) noexcept;
    void putValue(float value, const std::string& name, const std::string& unit, Format const& fmt) noexcept;
    void putValue(double value, const std::string& name, const std::string& unit, Format const& fmt) noexcept;
    void putValue(bool value, const std::string& name, const std::string& /*ignore*/, Format const& fmt) noexcept;
    void putValue(const std::string& value, const std::string& name, const std::string& /*ignore*/, Format const& fmt) noexcept;
    void putValue(const char* value, const std::string& name, const std::string& /*ignore*/, Format const& fmt) noexcept;
#if 0
    void putValue(const nosal::core::Span<const nosal::core::Byte>& value,
        const char* name,
        const char* /*ignore*/,
        Format const& fmt) noexcept;
#endif
    internal::LogReturnValue logret_{internal::LogReturnValue::kReturnOk};
    void* logcontext_;
    void* logdata_;
};

// Support for LogStream objects as rvalue reference. Enables custom type <<operator overload as first call
template <typename T>
LogStream& operator<<(LogStream&& out, T&& value) noexcept
{
    return (out << std::forward<T>(value));
}

// Support for signed long long (transforms into int64_t on 64 bit platforms)
template <typename T = void,
    typename std::enable_if<((!std::is_same<long long, int64_t>::value) && (sizeof(long long) == sizeof(int64_t))),
        T>::type* = nullptr>
inline LogStream& operator<<(LogStream& out, long long value) noexcept
{
    return (out << static_cast<int64_t>(value));
}

// Support for signed long (transforms into int32_t on 32 bit platforms)
template <typename T = void,
    typename std::enable_if<((!std::is_same<long, int32_t>::value) && (sizeof(long) == sizeof(int32_t))),
        T>::type* = nullptr>
inline LogStream& operator<<(LogStream& out, long value) noexcept
{
    return (out << static_cast<int32_t>(value));
}

// Support for unsigned long long (transforms into uint64_t on 64 bit platforms)
template <typename T = void,
    typename std::enable_if<((!std::is_same<unsigned long long, uint64_t>::value)
                                && (sizeof(unsigned long long) == sizeof(uint64_t))),
        T>::type* = nullptr>
inline LogStream& operator<<(LogStream& out, unsigned long long value) noexcept
{
    return (out << static_cast<uint64_t>(value));
}

// Support for unsigned long (transforms into uint32_t on 32 bit platforms)
template <typename T = void,
    typename std::enable_if<((!std::is_same<unsigned long, uint32_t>::value)
                                && (sizeof(unsigned long) == sizeof(uint32_t))),
        T>::type* = nullptr>
inline LogStream& operator<<(LogStream& out, unsigned long value) noexcept
{
    return (out << static_cast<uint32_t>(value));
}

/// @brief Writes LogLevel enum parameter as text into message.
LogStream& operator<<(LogStream& out, LogLevel value) noexcept;

constexpr LogHex32 loghex(uint32_t val) noexcept
{
    return LogHex32{val};
}
constexpr LogHex64 loghex(uint64_t val) noexcept
{
    return LogHex64{val};
}

/// @brief Writes pointer address into message, formatted as hexadecimal.
inline LogStream& operator<<(LogStream& out, const void* const value) noexcept
{
    return (out << loghex(reinterpret_cast<std::uintptr_t>(value)));
}
#if 0
/// @brief Writes std::string into message.
inline LogStream& operator<<(LogStream& out, const std::string& value) noexcept
{
    return (out << value.c_str());
}
#endif
template <typename Rep, typename Period>
inline LogStream& operator<<(LogStream& out, const std::chrono::duration<Rep, Period>& value) noexcept
{
    auto getUnit = []() -> std::string {
        if (std::is_same<typename Period::type, std::nano>::value)
            return "ns";
        if (std::is_same<typename Period::type, std::micro>::value)
            return "us";
        if (std::is_same<typename Period::type, std::milli>::value)
            return "ms";
        if (std::is_same<typename Period::type, std::ratio<1>>::value)
            return "s";
        if (std::is_same<typename Period::type, std::centi>::value)
            return "cs";
        if (std::is_same<typename Period::type, std::deci>::value)
            return "ds";

        std::ostringstream otherUnit;
        otherUnit << "(" << Period::num << "/" << Period::den << ")s";
        return otherUnit.str();
    };

    std::ostringstream s;
    s << std::fixed;
    s << value.count() << getUnit();

    return out << s.str();
}

/**
 * @brief Write a core::InstanceSpecifier into the message
 *
 * The InstanceSpecifier shall be shown as the result of calling InstanceSpecifier::ToString.
 *
 * @param out the LogStream object into which to add the value
 * @param value the InstanceSpecifier to log
 * @returns out
 */
//gcy LogStream& operator<<(LogStream& out, const nosal::core::InstanceSpecifier& value) noexcept;
/// @}

}  // namespace log
}  // namespace nosal
}  // namespace netaos

#endif  // NOSAL_LOG_LOG_STREAM_H_
